package com.rmkj.trade.match.core;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.function.Function;
import java.util.regex.Pattern;

/**
 * Created by Administrator on 2019/1/10.
 *
 * <p>demo:</p>
 * <pre>{@code
 *     TradingPeriods tradingPeriods = new TradingPeriods();
 *     tradingPeriods.begin("09:30").end("11:30").begin("14:30").end("17:30").begin("18:30").end("02:30");
 *     tradingPeriods.isOpen();
 *     tradingPeriods.isOpen(new Date());
 * }</pre>
 *
 * @author Administrator
 * @since JDK1.8
 */
public final class TradingPeriods {

	private List<String[]> periods = new ArrayList<>();
	private List<int[]> periods2 = new ArrayList<>();

	private volatile boolean nextBegin = true;

	private boolean over = false;

	private ValidateDay validateDay;

	public static final ValidateDay DefaultValidateDay = new DefaultValidateDay();

	public TradingPeriods() {
	}

	/**
	 * <p>convert {@link java.time.LocalDateTime} from {@link java.util.Date}</p>
	 * @param localDateTime
	 * @return
	 */
	public static Date toDate(LocalDateTime localDateTime){
		Objects.requireNonNull(localDateTime);
		return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
	}

	/**
	 * <p>convert {@link java.util.Date} from {@link java.time.LocalDateTime}</p>
	 * @param date
	 * @return
	 */
	public static LocalDateTime toLocalDateTime(Date date){
		Objects.requireNonNull(date);
		return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
	}

	public TradingPeriods(ValidateDay validateDay) {
		this.validateDay = validateDay;
	}

	public ValidateDay getValidateDay() {
		return validateDay;
	}

	/**
	 *
	 * @param validateDay
	 */
	public void setValidateDay(ValidateDay validateDay) {
		this.validateDay = validateDay;
	}

	/**
	 * judgment based on a given time
	 * @param localDateTime
	 * @return {@code new Object[]{true, "01:11"}} 是否开盘,下一个时间点（开盘或者休市时间点）
	 */
	public Object[] isOpen(LocalDateTime localDateTime){
		if(localDateTime != null){
			boolean dayValid = validateDay == null || validateDay.valid(localDateTime);

			Iterator<int[]> iterator = periods2.iterator();
			int[] lastPeriod = periods2.size() == 0 ? null : periods2.get(periods2.size()-1);
			while (iterator.hasNext()){
				int[] it = iterator.next();
				int t = parse(String.format("%02d:%02d", localDateTime.getHour(), localDateTime.getMinute()));
				// use t1 when compare with the end of period
				int t1 = t + (localDateTime.getSecond() > 0 ? 1 : 0);
				if (over && !iterator.hasNext()) {
					// 最后一个时间段，并且跨天
					if(t1 <= it[1]){
						// 验证跨天时间段，要看前一天是否开市
						boolean previousDayValid = validateDay == null || validateDay.valid(localDateTime.plusDays(-1));
						if(previousDayValid){
							return new Object[]{true, it[1]};
						}else{
							return new Object[]{false, it[1]};
						}
					}else if(t1 > it[1] && t < it[0]){
						return new Object[]{false, it[0]};
					}else if(t >= it[0]){
						if (dayValid) {
							return new Object[]{true, it[1]};
						}else{
							return new Object[]{false, it[1]};
						}
					}
				}else{
					// 其他正常时间段（开始时间小于结束时间）
					if(t < it[0] && (!over || (over && t1 > lastPeriod[1]))){
						return new Object[]{false, it[0]};
					}else if(t >= it[0] && t1 <= it[1]){
						if (dayValid) {
							return new Object[]{true, it[1]};
						}else{
							return new Object[]{false, it[1]};
						}
					}
				}
			}
		}
		return new Object[]{false, null};
	}

	/**
	 * judgment based on a given time
	 * @param date
	 * @return
	 * @deprecated
	 * @see #isOpen(LocalDateTime)
	 */
	public boolean isOpen(Date date){
		return (boolean)isOpen(date == null ? null : toLocalDateTime(date))[0];
	}

	/**
	 * judgment based on current system time
	 * @return
	 */
	public boolean isOpen(){
		return (boolean)isOpen(LocalDateTime.now())[0];
	}

	private final Timer timer = new Timer();

	/**
	 * start a {@link TimerTask}, it call function when start or the begin and end of the period
	 * @param function
	 */
	public void startListen(Function<Boolean, Void> function){
		startListen(function, null);
	}

	/**
	 * cancel all {@link TimerTask}
	 */
	public void shutdownListen(){
		timer.cancel();
	}

	private void startListen(Function<Boolean, Void> function, LocalDateTime localDateTime){
		Objects.requireNonNull(function);
		Object[] objects = isOpen(localDateTime == null ? LocalDateTime.now() : localDateTime);
		function.apply((Boolean) objects[0]);

		// 按照周期时间点，定点调用
		Integer hourMinute = (Integer) objects[1];
		if (hourMinute != null) {
			LocalDateTime now = LocalDateTime.now();
			LocalDateTime nextLocalDateTime = now.withHour(hourMinute.intValue()/100).withMinute(hourMinute.intValue()%100);
			LocalDateTime nextDate = nextLocalDateTime.isBefore(now) ? nextLocalDateTime.plusDays(1) : nextLocalDateTime;
			Date next = toDate(nextDate.withSecond(0));
//			System.out.println(next.toLocaleString());
			timer.schedule(new TimerTask(){

				@Override
				public void run() {
					startListen(function, nextLocalDateTime.withSecond(01));
				}

			}, next);
		}
	}


	/**
	 *
	 * @param time a string like '09:33'
	 * @return
	 */
	public TradingPeriods begin(String time){
		return add(time, true);
	}

	/**
	 *
	 * @param time a string like '09:33'
	 * @return
	 */
	public TradingPeriods end(String time){
		return add(time, false);
	}

	/**
	 * return true when the end-time of the last time-period is in next day, and cannot add any time-period
	 * @return
	 */
	public boolean isOver(){
		return over;
	}

	/**
	 * return true when the last time-period is complete
	 * @return
	 */
	public boolean isClosed(){
		return nextBegin;
	}

	public int count(){
		return periods.size() - (nextBegin ? 0 : 1);
	}

	public interface ValidateDay {
		boolean valid(LocalDateTime localDateTime);
	}

	private static final class DefaultValidateDay implements ValidateDay {

		@Override
		public boolean valid(LocalDateTime localDateTime) {
			if(localDateTime == null || localDateTime.getDayOfWeek().getValue() == 6 || localDateTime.getDayOfWeek().getValue() == 0){
				return false;
			}else{
				return true;
			}
		}
	}

	private synchronized TradingPeriods add(String time, boolean isBegin){
		if (validateTime(time)
				&& isBegin == nextBegin && !over) {
			int size = periods.size();
			if((nextBegin && size != 0 && !is2Greater1(periods.get(size-1)[1], time))
					|| (!nextBegin && periods.get(size-1)[0] == time)
					|| (!nextBegin && !is2Greater1(periods.get(size-1)[0], time) && size > 1 && is2Greater1(periods.get(0)[0], time))){
				return this;
			}

			if(nextBegin){
				periods.add(new String[]{time, null});
			}else{
				periods.get(size-1)[1] = time;

				// 成对后放入
				periods2.add(new int[]{parse(periods.get(size-1)[0]), parse(periods.get(size-1)[1])});
			}

			if(!nextBegin && !is2Greater1(periods.get(size-1)[0], time)){
				over = true;
			}
			nextBegin = !nextBegin;
		}
		return this;
	}

	/**
	 *
	 * @param time
	 * @return
	 */
	private boolean validateTime(String time){
		return Pattern.matches(MatchTradeConstants.REGULAR_EXPRESSION.HHMM, time);
	}

	private boolean is2Greater1(String time1, String time2){
		return parse(time2) > parse(time1);
	}

	/**
	 * convert the time string  to int, for example {@code parse("10:30") = 1030}
	 * @param time
	 * @return
	 */
	private int parse(String time){
		return Integer.parseInt(time.replace(":", ""));
	}

}
